/*
 * Copyright (C) 2009 eXo Platform SAS.
 *
 * This is free software; you can redistribute it and/or modify it
 * under the terms of the GNU Lesser General Public License as
 * published by the Free Software Foundation; either version 2.1 of
 * the License, or (at your option) any later version.
 *
 * This software is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this software; if not, write to the Free
 * Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
 * 02110-1301 USA, or see the FSF site: http://www.fsf.org.
 */
package org.everrest.core.impl.provider.json;

import org.everrest.core.impl.provider.json.JsonUtils.JsonToken;

import java.io.CharArrayWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PushbackReader;
import java.io.Reader;

/**
 * @author <a href="mailto:andrew00x@gmail.com">Andrey Parfonov</a>
 * @version $Id$
 */
public class JsonParser
{

   /** JsonHandler will serve events from parser. */
   private final JsonHandler eventHandler;

   /** Stack of JSON tokens. */
   private final JsonStack<JsonToken> stack;

   /** @see {@link java.io.PushbackReader}. */
   private PushbackReader pushbackReader;

   public JsonParser()
   {
      this(new JsonHandler());
   }

   protected JsonParser(JsonHandler eventHandler)
   {
      this.eventHandler = eventHandler;
      stack = new JsonStack<JsonToken>();
   }

   /** {@inheritDoc} */
   public void parse(Reader reader) throws JsonException
   {
      pushbackReader = new PushbackReader(reader);
      eventHandler.reset();
      stack.clear();
      char c;
      while ((c = next()) != 0)
      {
         if (c == '{')
         {
            readObject();
         }
         else if (c == '[')
         {
            readArray();
         }
         else
         {
            throw new JsonException("Syntax error. Unexpected '" + c + "'. Must be '{'.");
         }
      }
      if (!stack.isEmpty())
      {
         throw new JsonException("Syntax error. Missing one or more close bracket(s).");
      }
   }

   /** {@inheritDoc} */
   public void parse(InputStream stream) throws JsonException
   {
      parse(new InputStreamReader(stream, JsonUtils.DEFAULT_CHARSET));
   }

   /**
    * Get result of parsing.
    *
    * @return parsed JSON value
    */
   public JsonValue getJsonObject()
   {
      return eventHandler.getJsonObject();
   }

   /**
    * Read JSON object token, it minds all characters from '{' to '}'.
    *
    * @throws JsonException if JSON document has wrong format or i/o error
    * occurs.
    */
   private void readObject() throws JsonException
   {
      char c;
      // inform handler about start of object
      eventHandler.startObject();
      stack.push(JsonToken.object);
      for (; ; )
      {
         switch (c = next())
         {
            case 0:
               throw new JsonException("Syntax error. Unexpected end of object. Object must end by '}'.");
            case '{':
               readObject();
               break;
            case '}':
               // inform handler about end of object
               eventHandler.endObject();
               // check is allowed end of object now
               if (JsonToken.object != stack.pop())
               {
                  throw new JsonException("Syntax error. Unexpected end of object.");
               }
               // check is allowed char after end of json object
               switch (c = next())
               {
                  // end of stream
                  case 0:
                     break;
                  case ',':
                  case ']':
                  case '}':
                     back(c);
                     break;
                  default:
                     // must not happen
                     throw new JsonException("Syntax error. Expected " + "for ',' or ']' or '}' but found '" + c
                        + "'.");
               }
               // end for(;;)
               return;
            case '[':
               readArray();
               break;
            case ',':
               // nothing to do just must not be default is switch
               break;
            default:
               back(c);
               // all characters from start object to ':' - key.
               readKey();
               next(':');
               c = next();
               back(c);
               // object/array/value
               if (c != '{' && c != '[')
               {
                  readValue();
               }
               break;
         }
      }
   }

   /**
    * Read JSON array token, it minds all characters from '[' to ']'.
    *
    * @throws JsonException if JSON document has wrong format or i/o error
    * occurs.
    */
   private void readArray() throws JsonException
   {
      char c;
      // inform handler about start of array
      eventHandler.startArray();
      stack.push(JsonToken.array);
      for (; ; )
      {
         switch (c = next())
         {
            case 0:
               throw new JsonException("Syntax error. Unexpected end of array. Array must end by ']'.");
            case ']':
               // inform handler about end of array
               eventHandler.endArray();
               // check is allowed end of array now
               if (JsonToken.array != stack.pop())
               {
                  throw new JsonException("Syntax error. Unexpected end of array.");
               }
               // check is allowed char after end of json array
               switch (c = next())
               {
                  // end of stream
                  case 0:
                     break;
                  case ',':
                  case ']':
                  case '}':
                     back(c);
                     break;
                  default:
                     // must not happen
                     throw new JsonException("Syntax error. Expected for ',' or ']' or '}' but found '" + c + "'.");
               }
               // end for(;;)
               return;
            case '[':
               readArray();
               break;
            case '{':
               readObject();
               break;
            case ',':
               // nothing to do just must not be default
               break;
            default:
               back(c);
               readValue();
               break;
         }
      }
   }

   /**
    * Read key from stream.
    *
    * @throws JsonException if JSON document has wrong format or i/o error
    * occurs.
    */
   private void readKey() throws JsonException
   {
      char c = next();
      if (c != '"')
      {
         throw new JsonException("Syntax error. Key must start from quote, but found '" + c + "'.");
      }
      back(c);
      String s = new String(nextString());
      // if key as ""
      if (s.length() == 2)
      {
         throw new JsonException("Missing key.");
      }
      eventHandler.key(s.substring(1, s.length() - 1));
   }

   /**
    * Read value from stream.
    *
    * @throws JsonException if JSON document has wrong format or i/o error
    * occurs.
    */
   private void readValue() throws JsonException
   {
      char c = next();
      back(c);
      if (c == '"')
      {
         // value will be read as string
         eventHandler.characters(nextString());
      }
      else
      {
         // not string (numeric or boolean or null)
         CharArrayWriter cw = new CharArrayWriter();
         while ("{[,]}\"".indexOf(c = next()) < 0)
         {
            // Bug : WS-66
            if (c == 0)
            {
               throw new JsonException("Unexpected end of stream.");
            }
            cw.append(c);
         }
         back(c);
         eventHandler.characters(cw.toCharArray());
      }
      c = next(",]}");
      back(c);
   }

   /**
    * Get next char from stream, skipping whitespace and comments. Comments: One
    * line comment from // to end of line; Multi-line comments from / and * to *
    * and /
    *
    * @return the next char.
    * @throws JsonException if JSON document has wrong format or i/o error
    * occurs.
    */
   private char next() throws JsonException
   {
      try
      {
         int c;
         while ((c = pushbackReader.read()) != -1)
         {
            if (c == '/')
            {
               c = pushbackReader.read();
               if (c == '/')
               {
                  do
                  {
                     c = pushbackReader.read();
                  }
                  while (c != -1 && c != '\n' && c != '\r');
               }
               else if (c == '*')
               {
                  for (; ; )
                  {
                     c = pushbackReader.read();
                     if (c == '*')
                     {
                        c = pushbackReader.read();
                        if (c == '/')
                        {
                           break;
                        }
                     }
                     if (c == -1)
                     {
                        throw new JsonException("Syntax error. Missing end of comment.");
                     }
                  }
               }
               else
               {
                  back((char)c);
                  return '/';
               }
            }
            else if (c > ' ')
            {
               break;
            }
         }
         return (c == -1) ? 0 : (char)c;
      }
      catch (IOException e)
      {
         throw new JsonException(e.getMessage(), e);
      }
   }

   /**
    * Get next char from stream.
    *
    * @return the next char.
    * @throws JsonException if JSON document has wrong format or i/o error
    * occurs.
    */
   private char nextAny() throws JsonException
   {
      try
      {
         int c = pushbackReader.read();
         return (c == -1) ? 0 : (char)c;
      }
      catch (IOException e)
      {
         throw new JsonException(e.getMessage(), e);
      }
   }

   /**
    * Get next char from stream. And check is this char equals expected.
    *
    * @param c the expected char.
    * @return the next char.
    * @throws JsonException if JSON document has wrong format or i/o error
    * occurs.
    */
   private char next(char c) throws JsonException
   {
      char n = next();
      if (n != c)
      {
         throw new JsonException("Expected for '" + c + "' but found '" + n + "'.");
      }
      return n;
   }

   /**
    * Get next char from stream. And check is this char presents in given
    * string.
    *
    * @param s the string.
    * @return the next char.
    * @throws JsonException if JSON document has wrong format or i/o error
    * occurs.
    */
   private char next(String s) throws JsonException
   {
      char n = next();
      // if char present in string
      if (s.indexOf(n) >= 0)
      {
         return n;
      }
      // else error
      char[] ch = s.toCharArray();
      StringBuilder sb = new StringBuilder();
      int i = 0;
      for (char c : ch)
      {
         if (i > 0)
         {
            sb.append(" or ");
         }
         i++;
         sb.append('\'').append(c).append('\'');
      }
      throw new JsonException("Expected for " + sb.toString() + " but found '" + n + "'.");
   }

   /**
    * Get next n characters from stream.
    *
    * @param n the number of characters.
    * @return the array of characters.
    * @throws JsonException if JSON document has wrong format or i/o error
    * occurs.
    */
   private char[] next(int n) throws JsonException
   {
      char[] buff = new char[n];
      try
      {
         int i = pushbackReader.read(buff);
         if (i == -1)
         {
            throw new JsonException("Unexpected end of stream.");
         }
         return buff;
      }
      catch (IOException e)
      {
         throw new JsonException(e.getMessage(), e);
      }
   }

   /**
    * Get array chars up to given and include it.
    *
    * @return the char array.
    * @throws JsonException if JSON document has wrong format or i/o error
    * occurs.
    */
   private char[] nextString() throws JsonException
   {
      CharArrayWriter cw = new CharArrayWriter();
      char c = nextAny(); // read '"'
      cw.append(c);
      for (; ; )
      {
         switch (c = nextAny())
         {
            case 0:
            case '\n':
            case '\r':
               throw new JsonException("Syntax error. Unterminated string.");
            case '\\':
               switch (c = nextAny())
               {
                  case 0:
                  case '\n':
                  case '\r':
                     throw new JsonException("Syntax error. Unterminated string");
                  case 'n':
                     cw.append('\n');
                     break;
                  case 'r':
                     cw.append('\r');
                     break;
                  case 'b':
                     cw.append('\b');
                     break;
                  case 't':
                     cw.append('\t');
                     break;
                  case 'f':
                     cw.append('\f');
                     break;
                  case 'u': // unicode
                     String s = new String(next(4));
                     cw.append((char)Integer.parseInt(s, 16));
                     break;
                  default:
                     cw.append(c);
                     break;
               }
               break;
            default:
               cw.append(c);
               if (c == '"')
               {
                  return cw.toCharArray();
               }
               break;
         }
      }
   }

   /**
    * Push back given char to stream.
    *
    * @param c the char for pushing back.
    * @throws JsonException if JSON document has wrong format or i/o error
    * occurs.
    */
   private void back(char c) throws JsonException
   {
      try
      {
         pushbackReader.unread(c);
      }
      catch (IOException e)
      {
         throw new JsonException(e.getMessage(), e);
      }
   }

}
